﻿#region Using Directives

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using Kongo.Properties;

#endregion Using Directives


namespace Kongo
{
    public partial class Variable : Node
    {
        #region Constants

        private const string SEPARATOR = "separator";
        private const string ELEMENT = "element";

        #endregion Constants


        #region Fields

        private static readonly Regex regex = new Regex(@"(?<separator>[\.\|:]*)(?<element>""[^""\\]*(?:\\.[^""\\]*)*""|[A-Za-z_][A-Za-z_0-9]*)");
        // Sample: variable.member1.member2|filter1:"arg1"|filter2:"arg2"
        //     --> variable
        //     --> .member1
        //     --> .member2
        //     --> |filter1
        //     --> :"arg1"
        //     --> |filter2
        //     --> :"arg2"

        private bool isConstant;
        private string key;
        private string[] members;
        private FilterInfo[] filters;

        #endregion Fields


        #region Methods

        public override void Render(Context context, TextWriter output)
        {
            object var;
            if (!TryEvaluate(context, false, out var))
            {
                // We couldn't evaluate the variable
                base.Render(context, output);
                return;
            }

            // This is an optimization for variables that are templates
            // that don't have filters. Instead of buffering them to a string
            // we can pump them right into the output stream.
            if (var is Template)
            {
                ((Template)var).Render(context, output);
                return;
            }

            output.Write(var);
        }


        public bool TryEvaluate(Context context, out object value)
        {
            return TryEvaluate(context, true, out value);
        }


        private bool TryEvaluate(Context context, bool renderTemplates, out object value)
        {
            value = null;
            object var = null;


            // First: resolve the variable
            if (isConstant)
                var = key; // The key is the var
            else if (!context.TryGetValue(key, out var))
                return false; // Couldn't find the item in context


            // Second: do any member lookup
            if (var == null && members.Length > 0)
                return false; // There can't be any members on a null variable
            else if (var != null)
                if (!TryMemberLookup(members, ref var))
                    return false; // Member lookup failed


            // Third: did we find a template?
            if (var is Template)
            {
                if (filters.Length == 0 && !renderTemplates)
                    goto END;

                var = ((Template)var).Render(context);
            }
            

            // Forth: pipe it through the filters
            foreach (FilterInfo fi in filters)
            {
                Filter handler = Template.GetFilter(fi.Key);
                if(handler == null)
                    throw new ArgumentException(Helpers.Format(Resource.Argument_ItemNotFound, fi.Key, "filter"));

                var = handler(var, fi.Arg);
            }

        END:
            value = var;
            return true;
        }


        public override string ToString()
        {
            return key;
        }


        private bool TryMemberLookup(string[] members, ref object value)
        {
            // Search order:
            //     Property named: name.
            //     Method named: name().
            //     Field named: name.
            //     String indexer: value["name"].

            Debug.Assert(members != null, "The 'members' argument was null.");
            Debug.Assert(value != null, "The 'value' argument was null.");
            foreach (string member in members)
            {
                // Stop looking if the current value is null
                if (value == null)
                    return false;

                try
                {
                    // Look for a property
                    PropertyInfo pi = value.GetType().GetProperty(member, BindingFlags.Instance | BindingFlags.Public);
                    if (pi != null)
                    {
                        value = pi.GetValue(value, null);
                        continue;
                    }
                }
                catch { }

                try
                {
                    // Look for a method
                    MethodInfo mi = value.GetType().GetMethod(member, Type.EmptyTypes);
                    if (mi != null && mi.IsPublic && !mi.IsStatic)
                    {
                        value = mi.Invoke(value, null);
                        continue;
                    }
                }
                catch { }


                try
                {
                    // Look for a field
                    FieldInfo fi = value.GetType().GetField(member, BindingFlags.Public | BindingFlags.Instance);
                    if (fi != null)
                    {
                        value = fi.GetValue(value);
                        continue;
                    }
                }
                catch { }


                try
                {
                    // Look for a string indexer
                    PropertyInfo pi = value.GetType().GetProperty("Item", new Type[] { typeof(String) });
                    if (pi != null)
                    {
                        MethodInfo mi = pi.GetGetMethod(false);
                        if (mi != null && !mi.IsStatic)
                        {
                            value = mi.Invoke(value, new object[] { member });
                            continue;
                        }
                    }
                    
                }
                catch { }

                return false;
            }

            return true;
        }

        #endregion Methods


        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Variable"/> class using the specified string.
        /// </summary>
        /// <param name="expression">String representing the variable name and optional filter expression.</param>
        public Variable(string expression)
        {
            Helpers.ThrowIfNullOrEmpty(expression, "expression");

            // Construct the variable parts
            Parser p = new Parser(this, expression);
            p.PunchItChewy();
        }

        #endregion Constructors


        #region Types

        private struct FilterInfo
        {
            public string Key;
            public string Arg;
        }

        #endregion Types
    }
}
